using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using GLWrap.MathExt;
using GLWrap.SceneObjects;
using ReAnimation.Exceptions;
using ReAnimation.Helpers.Common;
using ReAnimation.Helpers.WinForms;
using Sharp3D.Math.Core;
using Tao.OpenGl;
using Tao.Platform.Windows;
using System.Windows.Forms;

namespace GLWrap
{
    public class GlSceneManager : IGlSceneManager
    {
        public float Angle { get; set; }
        public float Left { get; set; }
        public float Right { get; set; }
        public float Top { get; set; }
        public float Bottom { get; set; }
        public float Near { get; set; }
        public float Far { get; set; }
        public int Width { get; private set; }
        public int Height { get; private set; }
        public SimpleOpenGlControl OpenGlControl
        {
            get
            {
                return _openGlControl;
            }
        }

        private IDictionary<int, SceneObject> _sceneObjectsById;
        private bool _awaitingSelection;
        private int _xSelection, _ySelection;
        private int _selectedId;
        private SimpleOpenGlControl _openGlControl;
        private bool _sceneObjectsChanged;

        public GlSceneManager(ICamera camera)
        {
            _sceneObjectsById = new Dictionary<int, SceneObject>();
            Camera = camera;
            Near = 0.001f;
            Far = 10000f;
            Angle = 45.0f;
            _sceneObjectsChanged = false;
        }

        public void Initialize(SimpleOpenGlControl openGlControl)
        {
            InitOpenGlControl(openGlControl);

            int width = openGlControl.Width;
            int height = openGlControl.Height;

            Gl.glShadeModel(Gl.GL_SMOOTH);
            Gl.glEnable(Gl.GL_DEPTH_TEST);
            Gl.glEnable(Gl.GL_TEXTURE_2D);
            Gl.glEnable(Gl.GL_CULL_FACE);
            Gl.glEnable(Gl.GL_LIGHTING);
            Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);

            Gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

            var size = (float)(Near * Math.Tan(Angle.DegreesToRadians() * 0.5));

            Gl.glMatrixMode(Gl.GL_PROJECTION);
            Gl.glLoadIdentity();
            Gl.glFrustum(Left = -size,
                         Right =
                         size,
                         Bottom = -size / ((float)width / height),
                         Top = size / ((float)width / height),
                         Near,
                         Far);

            Gl.glViewport(0, 0, width, height);
            Width = width;
            Height = height;
            Gl.glMatrixMode(Gl.GL_MODELVIEW);
        }

        //TODO: implement Null object
        private void InitOpenGlControl(SimpleOpenGlControl openGlControl)
        {
            if (openGlControl == null) throw new ArgumentNullException("openGlControl");
            if (_openGlControl == openGlControl) return;

            if (_openGlControl != null)
            {
                _openGlControl.Paint -= OpenGlControlPaint;
                _openGlControl.Resize -= OpenGlControlResize;
                _openGlControl.MouseClick -= OpenGlControlMouseClick;
            }

            _openGlControl = openGlControl;
            _openGlControl.Paint += OpenGlControlPaint;
            _openGlControl.Resize += OpenGlControlResize;
            _openGlControl.MouseClick += OpenGlControlMouseClick;
        }

        void OpenGlControlMouseClick(object sender, MouseEventArgs e)
        {
            CheckSelectionAt(e.X, e.Y);
        }

        void OpenGlControlResize(object sender, EventArgs e)
        {
            Initialize(_openGlControl);
        }

        private void OpenGlControlPaint(object sender, PaintEventArgs e)
        {
            Paint();
        }
        //TODO: refactor
        public void Paint()
        {
            bool await = _awaitingSelection;
            if (_awaitingSelection)
            {
                _awaitingSelection = false;
                Gl.glDisable(Gl.GL_LIGHTING);
                PhysicallyPaint();
                InternalCheckSelection();
                Gl.glEnable(Gl.GL_LIGHTING);
            }
            PhysicallyPaint();
            Gl.glFlush();
            if (await)
            {
                if (_sceneObjectsById.ContainsKey(_selectedId))
                {
                    var obj = _sceneObjectsById[_selectedId];

                    if (obj != null)
                    {
                        obj.HaveBeenSelected();
                        SceneObjectSelected.Throw(this, new EventArg<SceneObject>(obj));
                    }
                }
            }
        }

        public void Refresh()
        {
            _openGlControl.Refresh();
        }

        public IList<SceneObject> SceneObjects
        {
            get { return new List<SceneObject>(_sceneObjectsById.Values); }
        }

        public ICamera Camera { get; set; }

        private void PhysicallyPaint()
        {
            Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
            Gl.glLoadIdentity();

            ApplyCamera();

            Gl.glTranslatef(0, 0, -1);
            
            IList<SceneObject> drawedObjects = new List<SceneObject>();
            IList<SceneObject> objectsToDraw = _sceneObjectsById.Values.ToList();

            int i = 0;
            while (i < objectsToDraw.Count)
            {
                _sceneObjectsChanged = false;
                drawedObjects.Add(objectsToDraw[i]);
                Gl.glPushMatrix();
                objectsToDraw[i++].Draw(this);
                Gl.glPopMatrix();

                if (_sceneObjectsChanged)
                {
                    objectsToDraw = _sceneObjectsById.Values.Except(drawedObjects).ToList();
                    i = 0;
                }
            }
        }

        

        private void ApplyCamera()
        {
            Glu.gluLookAt(
                Camera.Eye.X, Camera.Eye.Y, Camera.Eye.Z,
                Camera.View.X, Camera.View.Y, Camera.View.Z,
                Camera.Up.X, Camera.Up.Y, Camera.Up.Z
                );
        }

        public IGlSceneManager AddSceneObject(SceneObject sceneObject)
        {
            if (_sceneObjectsById.ContainsKey(sceneObject.ObjectId))
                return this;

            _sceneObjectsById.Add(sceneObject.ObjectId, sceneObject);
            sceneObject.AttachToScene(this);
            _sceneObjectsChanged = true;//????  ugly
            return this;
        }

        public IGlSceneManager RemoveSceneObject(SceneObject obj)
        {
            var keysToRemove = (from pair in _sceneObjectsById
                                where pair.Value.Equals(obj)
                                select pair.Key)
                                 .ToArray();

            foreach (var key in keysToRemove)
            {
                _sceneObjectsById[key].DetachFromScene(this);
                _sceneObjectsById.Remove(key);
                _sceneObjectsChanged = true;
            }

            return this;
        }

        public IGlSceneManager RemoveAllSceneObjects()
        {
            foreach (var sceneObject in _sceneObjectsById.Values)
                sceneObject.DetachFromScene(this);

            _sceneObjectsById.Clear();
            _sceneObjectsChanged = true;
            return this;
        }

        public bool IsAttached(SceneObject sceneObject)
        {
            return _sceneObjectsById.ContainsKey(sceneObject.ObjectId);
        }

        public event EventHandler<EventArg<SceneObject>> SceneObjectSelected;

        public void CheckSelectionAt(int x, int y)
        {
            _awaitingSelection = true;
            _xSelection = x;
            _ySelection = y;

            _openGlControl.Refresh();
        }

        public void InternalCheckSelection()
        {
            unsafe
            {
                Byte redComponent;
                Gl.glReadPixels(_xSelection, Height - _ySelection, 1, 1, Gl.GL_RGB, Gl.GL_UNSIGNED_BYTE,
                    new IntPtr(&redComponent));

                byte r = redComponent;
                _selectedId = r;

            }
        }
    }
}